#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>

#include <limits.h>

#define streq(str1, str2) (strcmp((str1), (str2)) == 0)

typedef struct FileEntry {
    char *relative_path;
    char *full_path;
    char *sec_full_path;
    char *new_old_path;
    struct FileEntry *next;
} FileEntry;

void add_file_entry(FileEntry **head, const char *relative_path, const char *full_path, const char *sec_full_path) {
    FileEntry *new_entry = malloc(sizeof(FileEntry));
    new_entry->relative_path = strdup(relative_path);
    new_entry->full_path = strdup(full_path);
	if (sec_full_path != NULL) 
		new_entry->sec_full_path = strdup(sec_full_path);
	else
		new_entry->sec_full_path = NULL;
    new_entry->next = *head;
    *head = new_entry;
}

FileEntry *find_in_list(FileEntry *list, const char *relative_path) {
    FileEntry *current = list;
    while (current != NULL) {
        if (streq(current->relative_path, relative_path)) {
            return current;
        }
        current = current->next;
    }

    return NULL;
}

void traverse_directory(const char *base_dir, const char *current_relative, FileEntry **list) {
    char current_dir[PATH_MAX];
    if (strlen(current_relative) == 0) {
        snprintf(current_dir, PATH_MAX, "%s", base_dir);
    } else {
        snprintf(current_dir, PATH_MAX, "%s/%s", base_dir, current_relative);
    }

    DIR *dir = opendir(current_dir);
    if (!dir) {
        perror("opendir");
        return;
    }

    struct dirent *entry;
    while ((entry = readdir(dir)) != NULL) {
		if (streq(entry->d_name, ".") || streq(entry->d_name, "..") || streq(entry->d_name, ".git")) {
			continue;
		}

        char child_relative[PATH_MAX];
        if (strlen(current_relative) == 0) {
            snprintf(child_relative, PATH_MAX, "%s", entry->d_name);
        } else {
            snprintf(child_relative, PATH_MAX, "%s/%s", current_relative, entry->d_name);
        }

        char child_full_path[PATH_MAX];
        snprintf(child_full_path, PATH_MAX, "%s/%s", base_dir, child_relative);

		// TODO Replace // with /
		char *p = child_full_path;
		while ((p = strstr(p, "//"))) {
			memmove(p, p + 1, strlen(p));
		}

        struct stat stat_buf;
        if (stat(child_full_path, &stat_buf) == -1) {
            perror("stat");
            continue;
        }

        if (S_ISDIR(stat_buf.st_mode)) {
            traverse_directory(base_dir, child_relative, list);
        } else if (S_ISREG(stat_buf.st_mode)) {
            add_file_entry(list, child_relative, child_full_path, NULL);
        }
    }

    closedir(dir);
}

int compare_files(const char *path1, const char *path2) {
    FILE *f1 = fopen(path1, "rb");
    FILE *f2 = fopen(path2, "rb");
    if (!f1 || !f2) {
        if (f1) fclose(f1);
        if (f2) fclose(f2);
        return -1;
    }

    int result = 0;
    int c1, c2;
    do {
        c1 = fgetc(f1);
        c2 = fgetc(f2);
        if (c1 != c2) {
            result = 1;
            break;
        }
    } while (c1 != EOF && c2 != EOF);

    if (result == 0) {
        if (fgetc(f1) != EOF || fgetc(f2) != EOF) {
            result = 1;
        }
    }

    fclose(f1);
    fclose(f2);
    return result;
}

int diff_num = 0;
int add_num = 0;
int del_num = 0;


void compare_lists(FileEntry *list_a, FileEntry *list_b,
                   FileEntry **diff_files, FileEntry **only_a, FileEntry **only_b) {
    FileEntry *a = list_a;
    while (a != NULL) {
        FileEntry *b_node = find_in_list(list_b, a->relative_path);
        if (b_node) {
            int cmp = compare_files(a->full_path, b_node->full_path);
            if (cmp == 1) {
                add_file_entry(diff_files, a->relative_path, a->full_path, b_node->full_path);
				diff_num++;
            }
        } else {
            add_file_entry(only_a, a->relative_path, a->full_path, NULL);
			add_num++;
        }

        a = a->next;
    }

    FileEntry *b = list_b;
    while (b != NULL) {
        FileEntry *a_node = find_in_list(list_a, b->relative_path);
        if (!a_node) {
            add_file_entry(only_b, b->relative_path, b->full_path, NULL);
			del_num++;
        }
        b = b->next;
    }
}

void print_list(const char *title, FileEntry *list) {
    printf("%s:\n", title);
    FileEntry *current = list;
    while (current != NULL) {
		if (current->sec_full_path == NULL) {
			printf("  %s\n", current->full_path);
		} else {
			printf("  %s %s\n", current->full_path, current->sec_full_path);
		}
        current = current->next;
    }

    printf("\n");
}

void free_list(FileEntry *list) {
    FileEntry *current = list;
    while (current != NULL) {
        FileEntry *next = current->next;
        free(current->relative_path);
        free(current->full_path);
        free(current);
        current = next;
    }
}

int flag_diff = 0;
int flag_add = 0;
int flag_del = 0;

int process_flag(int argc, char *argv[])
{
	int new_pos = 1; 

	for (int i = 1; i < argc; ++i) {
		if (streq(argv[i], "-add")) { 
			flag_add = 1;
		} else if (streq(argv[i], "-del")) {
			flag_del = 1;
		} else if (streq(argv[i], "-diff")) {
			flag_diff = 1;
		}else
			argv[new_pos++] = argv[i];
	}

	return new_pos; 
}

int main(int argc, char *argv[]) {

	argc = process_flag(argc, argv);
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <new-dir> <orig-dir> [ -add | -diff | -del ]\n", argv[0]);
        return 1;
    }

    const char *new_dir = argv[1];
    const char *orig_dir = argv[2];

    FileEntry *list_new = NULL;
    FileEntry *list_orig = NULL;

    traverse_directory(new_dir, "", &list_new);
    traverse_directory(orig_dir, "", &list_orig);

    FileEntry *diff_files = NULL;
    FileEntry *new_files = NULL;
    FileEntry *delete_files = NULL;

    compare_lists(list_new, list_orig, &diff_files, &new_files, &delete_files);

	if (flag_diff) 
		print_list("Different files", diff_files);

	if (flag_add)
		print_list("Add files", new_files);

	if (flag_del)
		print_list("Delete files", delete_files);

	printf("diff:%d add:%d delete:%d \n", diff_num, add_num, del_num);

    free_list(list_new);
    free_list(list_orig);
    free_list(diff_files);
    free_list(new_files);
    free_list(delete_files);

    return 0;
}
